/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package br.com.menosehmais.framework.persistence;

import br.com.menosehmais.framework.loggin.StackTraceLogger;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.persistence.metamodel.Attribute;
import javax.persistence.metamodel.EntityType;
import javax.persistence.metamodel.Metamodel;
import org.apache.log4j.Logger;

/**
 *
 * @author Rafael Quintino Gomes Rosa
 */
public class DaoJpa2< DO extends DomainObject > implements DataAccessObject<DO> {
    private Class<DO> domainClass;
    protected EntityManager entityManager;

    protected  Logger logger = Logger.getLogger(this.getClass());

    public DaoJpa2(Class<DO> domainClass, EntityManager entityManager){
        //ParameterizedType thisType = (ParameterizedType) getClass().getGenericSuperclass();
        //domainClass = (Class) thisType.getActualTypeArguments()[0];
        this.entityManager = entityManager;
        this.domainClass = domainClass;
        logger.debug(this.getClass().getName() + " instanciado");
    }

    @Override
    public List<DO> selectAll() {
        CriteriaBuilder criteriaBuilder = getEntityManager().getCriteriaBuilder();
        CriteriaQuery<DO> criteriaQuery = criteriaBuilder.createQuery(getDomainClass());
        criteriaQuery.from(getDomainClass());
        TypedQuery<DO> query = getEntityManager().createQuery(criteriaQuery);
        return query.getResultList();
    }

    @Override
    public List<DO> selectAll(int first, int max) {
        CriteriaBuilder criteriaBuilder = getEntityManager().getCriteriaBuilder();
        CriteriaQuery<DO> criteriaQuery = criteriaBuilder.createQuery(getDomainClass());
        criteriaQuery.from(getDomainClass());
        TypedQuery<DO> query = getEntityManager().createQuery(criteriaQuery);
        query.setFirstResult(first);
        query.setMaxResults(max);
        return query.getResultList();
    }

    @Override
    public DO selectById(String id){
        return getEntityManager().find(getDomainClass(), id);
    }

    protected Path getField(Root<DO> root, String field) {
        Path path = null;
        String fields[] = field.split("\\.");

        if(fields.length > 1) {
            path = root.get(fields[0]);
            for(int i = 1; i < fields.length; i++) {
                path = path.get(fields[i]);
            }
        }
        else {
            path = root.get(field);
        }
        
        return path;
    }

    protected <T> Path<T> getTemplateField(Root<DO> root, String field) {
        return (Path<T>) getField(root, field);
    }

    /**
     * Retorna uma lista de DO que foram encontrados usando o predicado AND.
     * @param attributes Map< nome_do_metodo, likes > onde likes é um Pattern a se encontrado, podendo-se utilizar os coringas _ ou %, sendo eles para representar um ou muitos caracteres respectivamente.
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    @Override //TODO - Renomear este método e testar o código
    public List<DO> findByStringFields(Map<String, String> fieldsAndLikesMap, boolean ignoreCase, int first, int max) {
    //public List<DO> findByAttributes(Map<String, String> attributes, int first, int max) {
        /* http://dominikdorn.com/2010/06/jpa2-abstract-dao-criteria-query-like-operator/ */

        CriteriaBuilder criteriaBuilder = getEntityManager().getCriteriaBuilder();
        CriteriaQuery<DO> criteriaQuery = criteriaBuilder.createQuery(getDomainClass());
        Root<DO> root = criteriaQuery.from(getDomainClass());

        List<Predicate> predicates = new ArrayList<Predicate>();
        for(Map.Entry<String, String> entry : fieldsAndLikesMap.entrySet())
        {
            Path path = this.getField(root, entry.getKey());
            if(path != null){
                Predicate predicate = null;

                if(ignoreCase)
                {
                    predicate = criteriaBuilder.like(criteriaBuilder.upper(path), entry.getValue().toUpperCase() );
                }
                else
                {
                    predicate = criteriaBuilder.like(path, entry.getValue() );
                }
                
                predicates.add(predicate);
            }
        }
        criteriaQuery.where(predicates.toArray(new Predicate[]{}));
        TypedQuery<DO> query = getEntityManager().createQuery(criteriaQuery);
        query.setFirstResult(first);
        query.setMaxResults(max);

        List<DO> results = query.getResultList();

        return results;
    }

    public List<DO> findByStringFieldsUsingPredicateOR(Map<String, String> fieldsAndLikesMap, boolean ignoreCase, int first, int max) {
        Set<DO> set = new HashSet<DO>();

        List<DO> doList = null;
        for(Map.Entry<String, String> entry : fieldsAndLikesMap.entrySet())
        {
            doList = findByStringField(entry.getKey(), entry.getValue(), ignoreCase, first, max);
            set.addAll(doList);
        }
        doList = new LinkedList<DO>();
        doList.addAll(set);

        return doList.subList(0, Math.min(doList.size(), max));
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    @Override
    public List<DO> findByStringField(String field, String likes, boolean ignoreCase, int first, int max) {
        CriteriaBuilder criteriaBuilder = getEntityManager().getCriteriaBuilder();
        CriteriaQuery<DO> criteriaQuery = criteriaBuilder.createQuery(getDomainClass());
        Root<DO> root = criteriaQuery.from(getDomainClass());

        List<Predicate> predicates = new ArrayList<Predicate>();

        Path path = this.getField(root, field);
        if(path != null){
            if(ignoreCase)
            {
                predicates.add(criteriaBuilder.like(criteriaBuilder.upper(path), likes.toUpperCase() ));
            }
            else
            {
                predicates.add(criteriaBuilder.like( path, likes ));
            }
        }

        criteriaQuery.where(predicates.toArray(new Predicate[]{}));
        TypedQuery<DO> query = getEntityManager().createQuery(criteriaQuery);
        query.setFirstResult(first);
        query.setMaxResults(max);

        List<DO> results = query.getResultList();

        return results;
    }

    /**
     * Retorna uma lista de Strings que foram encontradas.
     * 
     * @param field Atributo a ser utilizado que deve ser do tipo String.
     * @param likes Pattern a se encontrado, podendo-se utilizar os coringas _ ou %, sendo eles para representar um ou muitos caracteres respectivamente.
     * @param field : String - Nome do campo que se quer pesquisar
     * @param ignoreCase : boolean - Define se vai ser case sencitive.
     * @param first : int - primeiro resultado a ser considerado.
     * @param max : int - quantos resultados devem vir no máximo.

@return List<String> contendo os resultados.
     */
    @Override
    public List<String> selectStringListByFieldLikeString(String field, String likes, boolean ignoreCase, int first, int max){
        //TODO - Validar parâmetros de entrada.
        
        CriteriaBuilder criteriaBuilder = getEntityManager().getCriteriaBuilder();
        CriteriaQuery<String> criteriaQuery = criteriaBuilder.createQuery(String.class);
        Root<DO> root = criteriaQuery.from(getDomainClass());
        //criteriaQuery.select(root.<String>get(field));
        criteriaQuery.select( this.<String>getTemplateField(root, field) );
        if(ignoreCase)
        {
            //TODO - Tentar fazer mesma coisa do else só que com o ignoreCase!!!
            //criteriaQuery.where(criteriaBuilder.lower(root.<String>get(field)) criteriaBuilder.like(root.<String>get(field), "%" + likes + "%"));
            criteriaQuery.where(criteriaBuilder.like(criteriaBuilder.upper( this.<String>getTemplateField(root, field) ), likes.toUpperCase()));
        }
        else
        {
            criteriaQuery.where(criteriaBuilder.like(this.<String>getTemplateField(root, field), likes));
        }
        TypedQuery<String> query = getEntityManager().createQuery(criteriaQuery);
        query.setFirstResult(first);
        query.setMaxResults(max);

        List<String> results = query.getResultList();

        return results;
    }

    @Override
    public List<DO> selectUsingFilter(DO filter) { 
        return selectUsingFilter(filter, (String[])null);
    }

    @Override
    public List<DO> selectUsingFilter(DO filter, String... excludeProperties) {
        if(excludeProperties == null) {
            excludeProperties = new String[0];
        }
        Arrays.sort(excludeProperties);
        
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<DO> cq = cb.createQuery(domainClass);
        Root<DO> r = cq.from(domainClass);
        Predicate p = cb.conjunction();
        EntityType<DO> et = this.getDomainObjectMetaModel();
        Set<Attribute<? super DO, ?>> attrs = et.getAttributes();
        for (Attribute<? super DO, ?> a: attrs) {
            String name = a.getName();
            if(Arrays.binarySearch(excludeProperties, name) < 0){
                String javaName = a.getJavaMember().getName();
                String getter = "get" + javaName.substring(0,1).toUpperCase() + javaName.substring(1);
                try{
                    Method m = domainClass.getMethod(getter, (Class<?>[]) null);
                    if (m.invoke(filter, (Object[]) null) !=  null)
                    {
                        p = cb.and(p, cb.equal(r.get(name), m.invoke(filter, (Object[]) null)));
                    }
                } catch(Exception e){
                    StackTraceLogger.INSTANCE.printStackTrace(e);
                }
            }
        }
        cq.select(r).where(p);
        TypedQuery<DO> query = entityManager.createQuery(cq);
        return query.getResultList();
    }

    @Override
    public DO update(DO object) {
    	return getEntityManager().merge(object);
    }

    @Override
    public void updateUsingFilter(DO filter, DO object){
        //TODO
        throw new UnsupportedOperationException("Not supported yet.");
    }

    @Override
    public void deleteById(String id)  {
        DO object = this.selectById(id); //FIXME Melhorar este codigo para apenas setar o id no objeto e deleta-lo
        if(object == null)
        {
            //TODO - Exeção
        }
        this.delete(object);
    }

    @Override
    public void delete(DO object)  {
        String id = object.getId();

        getEntityManager().remove(object);
        logger.debug(object.getClass().getName()+".id="+id+" deletado.");
    }

    @Override
    public void deleteUsingFilter(DO filter){
        throw new UnsupportedOperationException("Not supported yet.");
    }



    @Override
    public DO insert(DO object){
        try{
        getEntityManager().persist(object);
        getEntityManager().flush();
        logger.debug(object.getClass().getName()+".id="+object.getId()+" persistido");
        }catch(RuntimeException ex){
            StackTraceLogger.INSTANCE.printStackTrace(ex);
            throw ex;
        }
        return object;
    }

    @Override
    public Long count() {
        CriteriaBuilder criteriaBuilder = getEntityManager().getCriteriaBuilder();
        CriteriaQuery<Long> criteriaQuery = criteriaBuilder.createQuery(Long.class);
        criteriaQuery.select(criteriaBuilder.count(criteriaQuery.from(getDomainClass())));
        return getEntityManager().createQuery(criteriaQuery).getSingleResult();
    }

    /**
     * @return the domainClass
     */
    public Class<DO> getDomainClass() {
        return domainClass;
    }

    /**
     * @param domainClass the domainClass to set
     */
    public void setDomainClass(Class<DO> domainClass) {
        this.domainClass = domainClass;
    }

    /**
     * @return the entityManager
     */
    public EntityManager getEntityManager() {
        return entityManager;
    }

    /**
     * @param entityManager the entityManager to set
     */
    public void setEntityManager(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    protected EntityType<DO> getDomainObjectMetaModel() {
        Metamodel metamodel = getEntityManager().getMetamodel();
        EntityType<DO> domainObject_ = metamodel.entity(domainClass);
        return domainObject_;
    }
}
